#ifndef MATRIXBUFFER_H
#define MATRIXBUFFER_H

#include <vector>
#include <limits>
#include "opengl_inc.h"
#include "material.h"
#include "matrix4.h"
#include "primitive.h"
#include "shadermanager.h"
#include "id_generator.h"
#include <cmath>

template < class v = VertexNoData  >
class OGLPrimitiveBuffer
{
public:
  typedef v Vertex;
  OGLPrimitiveBuffer() : bufferLength(0), initialized(false) {}
  void init()
  {
    initialized = true;
    glGenVertexArrays(1, &vao);
    glGenVertexArrays(1, &selvao);
    glGenVertexArrays(1, &flatvao);
    glGenBuffers(1, &idsBuffer);
    glGenBuffers(1, &materialsBuffer);
    glGenBuffers(1, &mMatricesBuffer);
    glGenBuffers(1, &nMatricesBuffer);
    initDrawVao();
    initSelectionVao();
    initFlatVao();
  }
  ~OGLPrimitiveBuffer() {}
  void deInit()
  {
    if (initialized)
    {
      deleteDrawVao();
      deleteSelectionVao();
      deleteFlatVao();
      glDeleteBuffers(1, &materialsBuffer);
      glDeleteBuffers(1, &nMatricesBuffer);
      glDeleteBuffers(1, &mMatricesBuffer);
      glDeleteBuffers(1, &idsBuffer);
      glDeleteVertexArrays(1, &vao);
      glDeleteVertexArrays(1, &selvao);
      glDeleteVertexArrays(1, &flatvao);
    }
  }
  void clear()
  {
    materials.clear();
    mMatrices.clear();
    nMatrices.clear();
    ids.clear();
  }
  void addPrimitive(const Material& mat, const Matrix4f mMatrix, const unsigned int idx)
  {
    materials.push_back(mat);
    mMatrices.push_back(mMatrix);
    nMatrices.push_back(mMatrix.toNormalMatrix());
    ids.push_back(idx);
  }
  bool hasPrimitives() const { return materials.size(); }
  void uploadData()
  {
    if (bufferLength < materials.size())
    {
      glBindBuffer(GL_ARRAY_BUFFER, materialsBuffer);
      glBufferData(GL_ARRAY_BUFFER, materials.size() * sizeof(Material), &(materials[0]), GL_STATIC_DRAW);
      glBindBuffer(GL_ARRAY_BUFFER, mMatricesBuffer);
      glBufferData(GL_ARRAY_BUFFER, mMatrices.size() * sizeof(Matrix4f), &(mMatrices[0]), GL_STATIC_DRAW);
      glBindBuffer(GL_ARRAY_BUFFER, nMatricesBuffer);
      glBufferData(GL_ARRAY_BUFFER, nMatrices.size() * sizeof(Matrix4f), &(nMatrices[0]), GL_STATIC_DRAW);
      glBindBuffer(GL_ARRAY_BUFFER, idsBuffer);
      glBufferData(GL_ARRAY_BUFFER, ids.size() * sizeof(GLuint), &(ids[0]), GL_STATIC_DRAW);
      bufferLength = materials.size();
    } else {   
      glBindBuffer(GL_ARRAY_BUFFER, materialsBuffer);
      glBufferSubData(GL_ARRAY_BUFFER, 0,  materials.size() * sizeof(Material), &(materials[0]));
      glBindBuffer(GL_ARRAY_BUFFER, mMatricesBuffer);
      glBufferSubData(GL_ARRAY_BUFFER, 0,  mMatrices.size() * sizeof(Matrix4f), &(mMatrices[0]));
      glBindBuffer(GL_ARRAY_BUFFER, nMatricesBuffer);
      glBufferSubData(GL_ARRAY_BUFFER, 0, nMatrices.size() * sizeof(Matrix4f), &(nMatrices[0]));
      glBindBuffer(GL_ARRAY_BUFFER, idsBuffer);
      glBufferSubData(GL_ARRAY_BUFFER, 0, ids.size() * sizeof(GLuint), &(ids[0]));
    }
  }
private:
  void initDrawVao()
  {
    glBindVertexArray(vao);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, primitive->indexBuff());
    glBindBuffer(GL_ARRAY_BUFFER, materialsBuffer);
    // and there is no hack, this is official way of doing this!
    glVertexAttribPointer(OGLShaderManager::MAT_AMBIENT, 4, GL_FLOAT, GL_FALSE, sizeof(Material), 0);
    glVertexAttribPointer(OGLShaderManager::MAT_DIFFUSE, 4, GL_FLOAT, GL_FALSE, sizeof(Material), (const GLvoid*)(sizeof(Vector4f)));
    glVertexAttribPointer(OGLShaderManager::MAT_SPECULAR, 4, GL_FLOAT, GL_FALSE, sizeof(Material), (const GLvoid*)(sizeof(Vector4f)*2));
    glVertexAttribPointer(OGLShaderManager::MAT_SHININESS, 1, GL_FLOAT, GL_FALSE, sizeof(Material), (const GLvoid*)(sizeof(Vector4f)*3));
    glVertexAttribDivisor(OGLShaderManager::MAT_AMBIENT, 1);
    glVertexAttribDivisor(OGLShaderManager::MAT_DIFFUSE, 1);
    glVertexAttribDivisor(OGLShaderManager::MAT_SPECULAR, 1);
    glVertexAttribDivisor(OGLShaderManager::MAT_SHININESS, 1);
    glEnableVertexAttribArray(OGLShaderManager::MAT_AMBIENT);
    glEnableVertexAttribArray(OGLShaderManager::MAT_DIFFUSE);
    glEnableVertexAttribArray(OGLShaderManager::MAT_SPECULAR);
    glEnableVertexAttribArray(OGLShaderManager::MAT_SHININESS);
    glBindBuffer(GL_ARRAY_BUFFER, mMatricesBuffer);
    glVertexAttribPointer(OGLShaderManager::M_MATRIX, 4, GL_FLOAT, GL_FALSE, sizeof(Matrix4f), 0);
    glVertexAttribPointer(OGLShaderManager::M_MATRIX+1, 4, GL_FLOAT, GL_FALSE, sizeof(Matrix4f), (const GLvoid*)(sizeof(Vector4f)));
    glVertexAttribPointer(OGLShaderManager::M_MATRIX+2, 4, GL_FLOAT, GL_FALSE, sizeof(Matrix4f), (const GLvoid*)(sizeof(Vector4f)*2));
    glVertexAttribPointer(OGLShaderManager::M_MATRIX+3, 4, GL_FLOAT, GL_FALSE, sizeof(Matrix4f), (const GLvoid*)(sizeof(Vector4f)*3));
    glVertexAttribDivisor(OGLShaderManager::M_MATRIX, 1);
    glVertexAttribDivisor(OGLShaderManager::M_MATRIX+1, 1);
    glVertexAttribDivisor(OGLShaderManager::M_MATRIX+2, 1);
    glVertexAttribDivisor(OGLShaderManager::M_MATRIX+3, 1);
    glEnableVertexAttribArray(OGLShaderManager::M_MATRIX);
    glEnableVertexAttribArray(OGLShaderManager::M_MATRIX+1);
    glEnableVertexAttribArray(OGLShaderManager::M_MATRIX+2);
    glEnableVertexAttribArray(OGLShaderManager::M_MATRIX+3);
    glBindBuffer(GL_ARRAY_BUFFER, nMatricesBuffer);
    glVertexAttribPointer(OGLShaderManager::N_MATRIX, 4, GL_FLOAT, GL_FALSE, sizeof(Matrix4f), 0);
    glVertexAttribPointer(OGLShaderManager::N_MATRIX+1, 4, GL_FLOAT, GL_FALSE, sizeof(Matrix4f), (const GLvoid*)(sizeof(Vector4f)));
    glVertexAttribPointer(OGLShaderManager::N_MATRIX+2, 4, GL_FLOAT, GL_FALSE, sizeof(Matrix4f), (const GLvoid*)(sizeof(Vector4f)*2));
    glVertexAttribPointer(OGLShaderManager::N_MATRIX+3, 4, GL_FLOAT, GL_FALSE, sizeof(Matrix4f), (const GLvoid*)(sizeof(Vector4f)*3));
    glVertexAttribDivisor(OGLShaderManager::N_MATRIX, 1);
    glVertexAttribDivisor(OGLShaderManager::N_MATRIX+1, 1);
    glVertexAttribDivisor(OGLShaderManager::N_MATRIX+2, 1);
    glVertexAttribDivisor(OGLShaderManager::N_MATRIX+3, 1);
    glEnableVertexAttribArray(OGLShaderManager::N_MATRIX);
    glEnableVertexAttribArray(OGLShaderManager::N_MATRIX+1);
    glEnableVertexAttribArray(OGLShaderManager::N_MATRIX+2);
    glEnableVertexAttribArray(OGLShaderManager::N_MATRIX+3);
    glBindBuffer(GL_ARRAY_BUFFER, primitive->vertexBuff());
    glVertexAttribPointer(OGLShaderManager::VERTEX, 4, GL_FLOAT, GL_FALSE, sizeof(Vertex), 0);
    glVertexAttribPointer(OGLShaderManager::NORMAL, 4, GL_FLOAT, GL_FALSE, sizeof(Vertex), (const GLvoid*)(sizeof(Vector4f)));
    glEnableVertexAttribArray(OGLShaderManager::VERTEX);
    glEnableVertexAttribArray(OGLShaderManager::NORMAL);
    if (Vertex::hasData())
    {
      glVertexAttribPointer(OGLShaderManager::DATA, 4, GL_FLOAT, GL_FALSE, sizeof(Vertex), (const GLvoid*)(sizeof(Vector4f)*2));
      glEnableVertexAttribArray(OGLShaderManager::DATA);
    }
    glBindVertexArray(0);
  }
  void deleteDrawVao()
  {
    glBindVertexArray(vao);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
    glDisableVertexAttribArray(OGLShaderManager::MAT_AMBIENT);
    glDisableVertexAttribArray(OGLShaderManager::MAT_DIFFUSE);
    glDisableVertexAttribArray(OGLShaderManager::MAT_SPECULAR);
    glDisableVertexAttribArray(OGLShaderManager::MAT_SHININESS);
    glDisableVertexAttribArray(OGLShaderManager::M_MATRIX);
    glDisableVertexAttribArray(OGLShaderManager::M_MATRIX+1);
    glDisableVertexAttribArray(OGLShaderManager::M_MATRIX+2);
    glDisableVertexAttribArray(OGLShaderManager::M_MATRIX+3);
    glDisableVertexAttribArray(OGLShaderManager::N_MATRIX);
    glDisableVertexAttribArray(OGLShaderManager::N_MATRIX+1);
    glDisableVertexAttribArray(OGLShaderManager::N_MATRIX+2);
    glDisableVertexAttribArray(OGLShaderManager::N_MATRIX+3);
    glDisableVertexAttribArray(OGLShaderManager::VERTEX);
    glDisableVertexAttribArray(OGLShaderManager::NORMAL);
    if (Vertex::hasData())
    {
      glDisableVertexAttribArray(OGLShaderManager::DATA);
    }
    glBindVertexArray(0);
  }
  void initSelectionVao()
  {
    glBindVertexArray(selvao);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, primitive->indexBuff());
    glBindBuffer(GL_ARRAY_BUFFER, idsBuffer);
    glVertexAttribIPointer(OGLShaderManager::ID, 1, GL_UNSIGNED_INT, 0, 0);
    glVertexAttribDivisor(OGLShaderManager::ID, 1);
    glEnableVertexAttribArray(OGLShaderManager::ID);
    glBindBuffer(GL_ARRAY_BUFFER, primitive->vertexBuff());
    glVertexAttribPointer(OGLShaderManager::VERTEX, 4, GL_FLOAT, GL_FALSE, sizeof(Vertex), 0);
    glEnableVertexAttribArray(OGLShaderManager::VERTEX);
    glBindBuffer(GL_ARRAY_BUFFER, mMatricesBuffer);
    glVertexAttribPointer(OGLShaderManager::M_MATRIX, 4, GL_FLOAT, GL_FALSE, sizeof(Matrix4f), 0);
    glVertexAttribPointer(OGLShaderManager::M_MATRIX+1, 4, GL_FLOAT, GL_FALSE, sizeof(Matrix4f), (const GLvoid*)(sizeof(Vector4f)));
    glVertexAttribPointer(OGLShaderManager::M_MATRIX+2, 4, GL_FLOAT, GL_FALSE, sizeof(Matrix4f), (const GLvoid*)(sizeof(Vector4f)*2));
    glVertexAttribPointer(OGLShaderManager::M_MATRIX+3, 4, GL_FLOAT, GL_FALSE, sizeof(Matrix4f), (const GLvoid*)(sizeof(Vector4f)*3));
    glVertexAttribDivisor(OGLShaderManager::M_MATRIX, 1);
    glVertexAttribDivisor(OGLShaderManager::M_MATRIX+1, 1);
    glVertexAttribDivisor(OGLShaderManager::M_MATRIX+2, 1);
    glVertexAttribDivisor(OGLShaderManager::M_MATRIX+3, 1);
    glEnableVertexAttribArray(OGLShaderManager::M_MATRIX);
    glEnableVertexAttribArray(OGLShaderManager::M_MATRIX+1);
    glEnableVertexAttribArray(OGLShaderManager::M_MATRIX+2);
    glEnableVertexAttribArray(OGLShaderManager::M_MATRIX+3);
    glBindVertexArray(0);
  }
  void deleteSelectionVao()
  {
    glBindVertexArray(selvao);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
    glDisableVertexAttribArray(OGLShaderManager::ID);
    glDisableVertexAttribArray(OGLShaderManager::VERTEX);
    glDisableVertexAttribArray(OGLShaderManager::M_MATRIX);
    glDisableVertexAttribArray(OGLShaderManager::M_MATRIX+1);
    glDisableVertexAttribArray(OGLShaderManager::M_MATRIX+2);
    glDisableVertexAttribArray(OGLShaderManager::M_MATRIX+3);
    glBindVertexArray(0);
  }
  void initFlatVao()
  {
    glBindVertexArray(flatvao);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, primitive->indexBuff());
    glBindBuffer(GL_ARRAY_BUFFER, primitive->vertexBuff());
    glVertexAttribPointer(OGLShaderManager::VERTEX, 4, GL_FLOAT, GL_FALSE, sizeof(Vertex), 0);  
    glEnableVertexAttribArray(OGLShaderManager::VERTEX);
    glBindBuffer(GL_ARRAY_BUFFER, mMatricesBuffer);
    glVertexAttribPointer(OGLShaderManager::M_MATRIX, 4, GL_FLOAT, GL_FALSE, sizeof(Matrix4f), 0);
    glVertexAttribPointer(OGLShaderManager::M_MATRIX+1, 4, GL_FLOAT, GL_FALSE, sizeof(Matrix4f), (const GLvoid*)(sizeof(Vector4f)));
    glVertexAttribPointer(OGLShaderManager::M_MATRIX+2, 4, GL_FLOAT, GL_FALSE, sizeof(Matrix4f), (const GLvoid*)(sizeof(Vector4f)*2));
    glVertexAttribPointer(OGLShaderManager::M_MATRIX+3, 4, GL_FLOAT, GL_FALSE, sizeof(Matrix4f), (const GLvoid*)(sizeof(Vector4f)*3));
    glVertexAttribDivisor(OGLShaderManager::M_MATRIX, 1);
    glVertexAttribDivisor(OGLShaderManager::M_MATRIX+1, 1);
    glVertexAttribDivisor(OGLShaderManager::M_MATRIX+2, 1);
    glVertexAttribDivisor(OGLShaderManager::M_MATRIX+3, 1);
    glEnableVertexAttribArray(OGLShaderManager::M_MATRIX);
    glEnableVertexAttribArray(OGLShaderManager::M_MATRIX+1);
    glEnableVertexAttribArray(OGLShaderManager::M_MATRIX+2);
    glEnableVertexAttribArray(OGLShaderManager::M_MATRIX+3);
    glBindVertexArray(0);
  }
  void deleteFlatVao()
  {
    glBindVertexArray(flatvao);
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
    glDisableVertexAttribArray(OGLShaderManager::VERTEX);
    glDisableVertexAttribArray(OGLShaderManager::M_MATRIX);
    glDisableVertexAttribArray(OGLShaderManager::M_MATRIX+1);
    glDisableVertexAttribArray(OGLShaderManager::M_MATRIX+2);
    glDisableVertexAttribArray(OGLShaderManager::M_MATRIX+3);
    glBindVertexArray(0);
  }

public:
  void draw()
  {
    if (hasPrimitives())
    {
      glBindVertexArray(vao);
      glDrawElementsInstanced(primitive->mode(), primitive->indices_count(), GL_UNSIGNED_INT, 0, mMatrices.size());
      glBindVertexArray(0);
    }
  }
  void drawSelection()
  {
    if (hasPrimitives())
    {
      glBindVertexArray(selvao);
      glDrawElementsInstanced(primitive->mode(), primitive->indices_count(), GL_UNSIGNED_INT, 0, mMatrices.size());
      glBindVertexArray(0);
    }
  }
  void drawFlat()
  {
    if (hasPrimitives())
    {
      glBindVertexArray(flatvao);
      glDrawElementsInstanced(primitive->mode(), primitive->indices_count(), GL_UNSIGNED_INT, 0, mMatrices.size());
      glBindVertexArray(0);
    }
  }

  void setPrimitive(OGLPrimitive<Vertex>* p)
  {
    primitive = p;
  }
#ifdef DEBUG
  void printData()
  {
    primitive->printData();
    std::cerr << "ModelM" << std::endl;
    printMatrices(MODEL);
    std::cerr << "NormalM" << std::endl;
    printMatrices(NORMAL);

    glBindBuffer(GL_TEXTURE_BUFFER, materialsBuffer);
    Material* materials_ = (Material*) glMapBuffer( GL_TEXTURE_BUFFER, GL_READ_ONLY);
    std::cerr << "Materials" << std::endl;
    for (unsigned int i = 0; i < materials.size(); ++i) std::cerr << materials_[i] << std::endl ;
    glUnmapBuffer( GL_TEXTURE_BUFFER);
    glBindBuffer(GL_TEXTURE_BUFFER, idsBuffer);
    GLuint* ids__ = (GLuint*) glMapBuffer( GL_TEXTURE_BUFFER, GL_READ_ONLY);
    std::cerr << "IDs" << std::endl;
    for (unsigned int i = 0; i < ids.size(); ++i) std::cerr << ids__[i] << " " ;
    glUnmapBuffer( GL_TEXTURE_BUFFER);
    glBindBuffer(GL_TEXTURE_BUFFER, 0);
    std::cerr << std::endl;
  }
  void printIDs()
  {
    glBindBuffer(GL_TEXTURE_BUFFER, idsBuffer);
    GLuint* ids__ = (GLuint*) glMapBuffer( GL_TEXTURE_BUFFER, GL_READ_ONLY);
    std::cerr << "IDs" << std::endl;
    for (unsigned int i = 0; i < ids.size(); ++i) std::cerr << ids__[i] << " " ;
    glUnmapBuffer( GL_TEXTURE_BUFFER);
    glBindBuffer(GL_TEXTURE_BUFFER, 0);
    std::cerr << std::endl;
  }
  enum MatrixType {MODEL, NORMAL};
  void printMatrices(MatrixType t)
  {
    GLuint buffer;
    switch (t)
    {
    case MODEL:
      buffer = mMatricesBuffer; break;
    case NORMAL:
      buffer = nMatricesBuffer; break;

    }

    glBindBuffer( GL_TEXTURE_BUFFER, buffer);
    Matrix4f* matrices = (Matrix4f*) glMapBuffer( GL_TEXTURE_BUFFER, GL_READ_ONLY);
    for (unsigned int i = 0; i < materials.size(); ++i) std::cerr << matrices[i] << std::endl << "-----------" << std::endl;
    glUnmapBuffer( GL_TEXTURE_BUFFER);
    glBindBuffer( GL_TEXTURE_BUFFER, 0);
  }

#endif
  
private:
  GLuint vao, selvao, flatvao, mtomvvao[2];
  GLuint idsBuffer;
  GLuint materialsBuffer;
  GLuint nMatricesBuffer;
  GLuint mMatricesBuffer;
  std::vector<Material> materials;
  std::vector<Matrix4f> mMatrices;
  std::vector<Matrix4f> nMatrices;
  std::vector<GLuint> ids;
  OGLPrimitive<Vertex>* primitive;
  unsigned int bufferLength;
  bool initialized;
};

#endif
